Form 데이터의 클라이언트 사이드 검증과 서버 사이드 검증

✒️ 2025-06-25 17:33 내용 수정


클라이언트 사이드 검증

참고 자료 : Craig Buckler's Form Validation Using JavaScript's Constraint Validation API, Geeksforgeeks JavaScript Form Validation, mdn web docs Client-side form validation

function validate(e) {
	const email = document.querySelector("#email").value.trim();
	const password = document.querySelector("#password").value;
	const emailError = document.querySelector("#emailError");
	const passwordError = document.querySelector("#passwordError");

	// 이메일 정규표현식
	const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;

	// 빈 값 확인
	if (!email || email === "" || /\d/.test(email)) {
		e.preventDefault();
		emailError.textContent = "이메일을 입력해주세요";
		return false;
	}

	// 형식 확인
    if (!emailRegex.test(email)) {
	    e.preventDefault();
        emailError.textContent = 
            "유효한 비밀번호 주소를 입력해주세요";
        return false;
    }        

	// 비밀번호 정규표현식
	// 8~16자, 숫자 1개 포함
	const passRegex = /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,16}$/;

	if (!password || password === "" || /\d/.test(password)) {
		e.preventDefault();
		passwordError.textContent = "비밀번호를 입력해주세요";
		return false;
	}

    if (!passRegex.test(password))  {
	    e.preventDefault();
        passwordError.textContent = 
            "비밀번호는 8~16자, 최소 숫자 1개를 포함해야 합니다";
        return false;
    }    

	return true;
}
<form action="/login" method="post"
onsubmit="return validate(e)">
	<label for="email">이메일</label>
	<input type="email" 
		name="email" id="email"
		required>
	<span id="emailError" sytle="color:red;"></span>

	<label for="password">비밀번호</label>
	<input type="password" 
		name="password" id="password"
		required>
	<span id="passwordError" sytle="color:red;"></span>
	
	<button type="submit">제출</button>
</form>

클라이언트 검증의 한계


서버 사이드 검증

데이터 무결성

public class User {
	private String name;
	private int age;
}
요청 : {name: "홍길동", age: "abc"} => X
요청 : {name: "홍길동", age: 20} => O
요청 : {age: 20} => X
요청 : {name: "홍길동", age: 20} => O
나이 : 0 ~ 150

요청 : {name: "홍길동", age : -1} => X
요청 : {name: "홍길동", age : 20} => O

커스텀 에러 메시지 사용

try {
	test();
} catch(Exception e) {
	// 에러 로그는 내부에만 출력
	System.out.println("에러 발생: " + e.getMessage());
	// 에러 발생 시 커스텀 에러 메시지를 따로 지정
	request.setAttribute("errorMessage", "에러가 발생했습니다.");
}

XSS 공격 방지

package util;  
  
import java.security.SecureRandom;  
import java.util.regex.Pattern;  
  
public class SecurityUtil {  
    private static final Pattern SCRIPT_PATTERN = 
    Pattern.compile("(?i)<script[^>]*>.*?</script>");  
  
    // XSS 방지 - HTML 태그 제거 및 이스케이프  
    public static String escapeHtml(String input) {  
        if (input == null) return null;  
  
        return input.replace("&", "&amp;")  
                .replace("<", "&lt;")  
                .replace(">", "&gt;")  
                .replace("\"", "&quot;")  
                .replace("'", "&#x27;")  
                .replace("/", "&#x2F;");  
    }  
  
    // 스크립트 태그 제거  
    public static String removeScripts(String input) {  
        if (input == null) return null;
        // Pattern과 Matcher를 사용한 정규 표현식 검증
        // script 태그가 포함된 경우 제거하기
        return SCRIPT_PATTERN.matcher(input).replaceAll("");  
    }  
}
User user = new User("홍길동");
Cookie cookie = new Cookie("user", user);

cookie.setHttpOnly(true); // JavaScript 접근 차단
cookie.setSecure(true); // HTTPS 전용
response.addCookie(cookie); // 응답 객체에 cookie 추가

CSRF 공격 방지

import java.security.SecureRandom;  
  
public class SecurityUtil {  
    // CSRF 토큰 생성  
    public static String generateCSRFToken() {  
	    // 랜덤한 값 생성
        SecureRandom random = new SecureRandom();  
        byte[] bytes = new byte[32];  
        random.nextBytes(bytes);  
        
        // token 문자열 생성
        StringBuilder token = new StringBuilder();  
        for (byte b : bytes) {  
            token.append(String.format("%02x", b));  
        }  
        return token.toString();  
    }    
}
// Servlet 예시
@WebServlet("/books/add")  
public class AddBookServlet extends HttpServlet {  
    private static final long serialVersionUID = 1L;  

	// 페이지 접근
    protected void doGet(
	    HttpServletRequest request, 
	    HttpServletResponse response)  
            throws ServletException, IOException {  
        // CSRF 토큰 생성  
        String csrfToken = SecurityUtil.generateCSRFToken();
        // cookie에 추가
        Cookie cookie = new Cookie("csrfToken", csrfToken);
        response.addCookie(cookie);

		// view 반환
        RequestDispatcher dispatcher = 
        request.getRequestDispatcher("/WEB-INF/views/addBook.jsp");  
        dispatcher.forward(request, response);  
    }  

	// 데이터 추가 요청
    protected void doPost(
	    HttpServletRequest request, 
	    HttpServletResponse response)  
            throws ServletException, IOException {  
  
        // Cookie에서 csrfToken 탐색
        Cookie[] cookies = request.getCookies();
		String csrfToken = null;  
		for(Cookie cookie : cookies) {  
		    if (cookie.getName().equals("csrfToken")) {  
		        csrfToken = cookie.getValue();  
		        break;  
		    }  
		}

		// DB나 Session 등에 따로 저장된 csrfToken
		String savedCsrfToken;

		// CSRF 토큰 검증  
		if (csrfToken == null || !savedCsrfToken.equals(csrfToken)) {  
		    request.setAttribute("errorMessage", "잘못된 요청입니다.");  
		    RequestDispatcher dispatcher = 
		    request.getRequestDispatcher("/WEB-INF/views/error.jsp");  
		    dispatcher.forward(request, response);  
		    return;  
		}

		// 검증 이후 동작 수행
    }  
}

SQL Injection 방지

공격 시나리오

String userId = request.getParameter("userId");
String sql = "SELECT * FROM users WHERE userId = " + userId;
-- OR 1=1은 항상 true
SELECT * FROM users WHERE userId = 0 OR 1=1;
-- ; DROP TABLE users;--
SELECT * FROM users WHERE userId = 0; DROP TABLE users;--

SQL문 보호

PreparedStatement pstmt = conn.prepareStatement(
  "SELECT * FROM users WHERE userId = ?");
pstmt.setInt(1, Integer.parseInt(userId));
SELECT * FROM users WHERE userId = 1